// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/webui/test_data_source.h"

#include <memory>

#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/memory/ref_counted_memory.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/url_constants.h"
#include "chrome/common/webui_url_constants.h"
#include "content/public/browser/url_data_source.h"
#include "content/public/common/url_constants.h"
#include "services/network/public/mojom/content_security_policy.mojom.h"

namespace {
const char kModuleQuery[] = "module=";
}  // namespace

TestDataSource::TestDataSource(std::string root) {
  base::FilePath test_data;
  CHECK(base::PathService::Get(chrome::DIR_TEST_DATA, &test_data));
  src_root_ = test_data.AppendASCII(root).NormalizePathSeparators();
  DCHECK(test_data.IsParent(src_root_));

  base::FilePath exe_dir;
  base::PathService::Get(base::DIR_EXE, &exe_dir);
  gen_root_ = exe_dir.AppendASCII("gen/chrome/test/data/" + root)
                  .NormalizePathSeparators();
  DCHECK(exe_dir.IsParent(gen_root_));

  custom_paths_ = {
      {"/chai.js", "third_party/chaijs/chai.js"},
      {"/mocha.js", "third_party/mocha/mocha.js"},
      {"/test_loader.html", "ui/webui/resources/html/test_loader.html"},
      {"/test_loader.js", "ui/webui/resources/js/test_loader.js"},
      {"/test_loader_util.js", "ui/webui/resources/js/test_loader_util.js"},
  };
}

TestDataSource::~TestDataSource() = default;

std::string TestDataSource::GetSource() {
  return "test";
}

void TestDataSource::StartDataRequest(
    const GURL& url,
    const content::WebContents::Getter& wc_getter,
    content::URLDataSource::GotDataCallback callback) {
  const std::string path = content::URLDataSource::URLToRequestPath(url);
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_BLOCKING},
      base::BindOnce(&TestDataSource::ReadFile, base::Unretained(this), path,
                     std::move(callback)));
}

std::string TestDataSource::GetMimeType(const std::string& path) {
  std::string clean_path = GetURLForPath(path).path();
  if (base::EndsWith(clean_path, ".html",
                     base::CompareCase::INSENSITIVE_ASCII) ||
      base::StartsWith(GetURLForPath(path).query(), kModuleQuery,
                       base::CompareCase::INSENSITIVE_ASCII)) {
    // Direct request for HTML, or autogenerated HTML response for module query.
    return "text/html";
  }
  // The test data source currently only serves HTML and JS.
  CHECK(base::EndsWith(clean_path, ".js", base::CompareCase::INSENSITIVE_ASCII))
      << "Tried to read file with unexpected type from test data source: "
      << path;
  return "application/javascript";
}

bool TestDataSource::ShouldServeMimeTypeAsContentTypeHeader() {
  return true;
}

bool TestDataSource::AllowCaching() {
  return false;
}

std::string TestDataSource::GetContentSecurityPolicy(
    network::mojom::CSPDirectiveName directive) {
  if (directive == network::mojom::CSPDirectiveName::ScriptSrc) {
    return "script-src chrome://* 'self';";
  } else if (directive == network::mojom::CSPDirectiveName::WorkerSrc) {
    return "worker-src blob: 'self';";
  } else if (directive ==
                 network::mojom::CSPDirectiveName::RequireTrustedTypesFor ||
             directive == network::mojom::CSPDirectiveName::TrustedTypes) {
    return std::string();
  } else if (directive == network::mojom::CSPDirectiveName::FrameAncestors) {
    return "frame-ancestors chrome://* 'self';";
  } else if (directive == network::mojom::CSPDirectiveName::FrameSrc) {
    return "frame-src chrome://test/;";
  } else if (directive == network::mojom::CSPDirectiveName::ChildSrc) {
    return "child-src chrome://test/;";
  }

  return content::URLDataSource::GetContentSecurityPolicy(directive);
}

GURL TestDataSource::GetURLForPath(const std::string& path) {
  return GURL(std::string(content::kChromeUIScheme) + "://" + GetSource() +
              "/" + path);
}

void TestDataSource::ReadFile(
    const std::string& path,
    content::URLDataSource::GotDataCallback callback) {
  std::string content;

  GURL url = GetURLForPath(path);
  CHECK(url.is_valid());

  // First check if a custom path mapping exists for the requested URL.
  auto it = custom_paths_.find(url.path());
  if (it != custom_paths_.end()) {
    base::FilePath src_root;
    CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_root));
    base::FilePath file_path =
        src_root.AppendASCII(it->second).NormalizePathSeparators();
    CHECK(base::ReadFileToString(file_path, &content))
        << url.spec() << "=" << file_path.value();
    scoped_refptr<base::RefCountedString> response =
        base::RefCountedString::TakeString(&content);
    std::move(callback).Run(response.get());
    return;
  }

  if (base::StartsWith(url.query(), kModuleQuery,
                       base::CompareCase::INSENSITIVE_ASCII)) {
    std::string js_path = url.query().substr(strlen(kModuleQuery));

    base::FilePath file_path =
        src_root_.Append(base::FilePath::FromUTF8Unsafe(js_path));
    // Do some basic validation of the JS file path provided in the query.
    CHECK_EQ(file_path.Extension(), FILE_PATH_LITERAL(".js"));

    base::FilePath file_path2 =
        gen_root_.Append(base::FilePath::FromUTF8Unsafe(js_path));
    CHECK(base::PathExists(file_path) || base::PathExists(file_path2))
        << url.spec() << "=" << file_path.value();
    content = "<script type=\"module\" src=\"" + js_path + "\"></script>";
  } else {
    // Strip the query
    size_t query_length = url.query().length();
    std::string no_query_path =
        query_length == 0 ? path
                          : path.substr(0, path.length() - query_length - 1);

    // Try the |gen_root_| folder first, covering cases where the test file is
    // generated at build time. We do this first as if a test file exists under
    // the same name in the src and gen directories, the generated file is
    // generally the desired file (for example, may have been preprocessed).
    base::FilePath gen_root_file_path =
        gen_root_.Append(base::FilePath::FromUTF8Unsafe(no_query_path));
    if (base::PathExists(gen_root_file_path)) {
      CHECK(base::ReadFileToString(gen_root_file_path, &content))
          << url.spec() << "=" << gen_root_file_path.value();
    } else {
      // Then try the |src_root_| folder, covering cases where the test file is
      // generated at build time.
      base::FilePath src_root_file_path =
          src_root_.Append(base::FilePath::FromUTF8Unsafe(no_query_path));
      CHECK(base::ReadFileToString(src_root_file_path, &content))
          << url.spec() << "=" << src_root_file_path.value();
    }
  }

  scoped_refptr<base::RefCountedString> response =
      base::RefCountedString::TakeString(&content);
  std::move(callback).Run(response.get());
}
